fix(http): validate Host and Origin for streamable-http and sse transports#382
Conversation
…ports Browsers can reach the streamable-http and sse endpoints via DNS rebinding and then invoke MCP tools under the server-side Azure / Kubernetes identity when OAuth is disabled. Add a transport-agnostic Host/Origin allowlist middleware in front of /mcp, /sse and /message, plus a startup-time validator that refuses to bring up a public HTTP listener with no authentication and no explicit trusted-host allowlist. - internal/server/httpsecurity: new middleware with loopback-only default allowlist, explicit allow entries (case-insensitive, with optional port), and a "*" escape valve for trusted reverse-proxy deployments. Empty Origin (non-browser callers) is accepted; any non-empty Origin must match the allowlist. Rejections return 403 with a JSON error body and a structured warning log. - internal/config: add --allowed-host / --trusted-origin flags and ValidateConfig() rejection of "streamable-http|sse + non-loopback bind + OAuth disabled + no --allowed-host" so insecure deployments fail closed at startup instead of silently exposing tools. - internal/server: wire the middleware around /mcp, /sse and /message (outside OAuth so foreign origins are rejected before any session work). /health stays outside the middleware so probes are unaffected. - chart: expose security.allowedHosts and security.trustedOrigins, threaded into deployment args. Defaults are empty so existing OAuth-enabled or ingress-fronted deployments are not surprised; insecure-default deployments CrashLoopBackOff via the new validator. - tests: cover the middleware decision matrix, the startup-time validator, and confirm the middleware is mounted on both transports.
…default When OAuth is enabled, a valid bearer token is already required for tool dispatch and DNS rebinding cannot produce one, so applying the Host/Origin allowlist would only break legitimate ingress / reverse-proxy deployments that forward the public hostname. The middleware now defaults to allow-any in that configuration; operators who still want belt-and-suspenders Host enforcement can opt back in via --allowed-host / --trusted-origin.
Backwards-compatibility impactThis change adds two new flags ( CLI users (
|
6572623 to
76b42c3
Compare
There was a problem hiding this comment.
Pull request overview
This PR hardens AKS-MCP’s HTTP-based transports (streamable-http and SSE) against DNS-rebinding / cross-origin abuse by adding Host/Origin enforcement middleware, startup-time “fail closed” validation for unsafe deployment combinations, and Helm values to configure the new policy. It also refines Azure CLI tool error handling to key off exit codes while treating stderr-only output as a warning.
Changes:
- Add
internal/server/httpsecuritymiddleware and wire it in front of/mcp,/sse, and/message(excluding/health). - Add config flags and
ValidateConfig()refusal for publicly reachable HTTP transports when OAuth is disabled and no host allowlist is set. - Expose the allowlists through Helm chart values and deployment args.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/server/server.go | Wires Host/Origin middleware into streamable-http + SSE endpoints and adds helper methods for testable mounting/default behavior. |
| internal/server/server_test.go | Adds tests that assert middleware is mounted and enforces expected defaults/overrides. |
| internal/server/httpsecurity/host_origin.go | Implements Host/Origin enforcement middleware with loopback defaults and JSON 403 responses. |
| internal/server/httpsecurity/host_origin_test.go | Unit tests for allowlist matching and middleware behavior (Host/Origin, wildcard, loopback). |
| internal/config/config.go | Adds --allowed-host / --trusted-origin parsing and startup-time refusal for unsafe public HTTP binds without auth/allowlist. |
| internal/config/config_test.go | Adds validation tests for unsafe vs safe transport/auth/bind combinations. |
| internal/components/azapi/handler.go | Switches Azure CLI failure detection to ExitCode != 0 and logs stderr as warning when exit code is 0. |
| internal/components/azapi/handler_test.go | Updates mocks for ExitCode and adds coverage for warning-only stderr results and exit code messaging. |
| chart/values.yaml | Adds security.allowedHosts / security.trustedOrigins values and documents startup refusal behavior. |
| chart/templates/deployment.yaml | Threads Helm security values into container args (--allowed-host, --trusted-origin). |
- isHostAllowed: match allowlist entries that carry an explicit port (e.g. "host:8000"), handle IPv6 bracketed forms with/without port, and still allow port-less entries to match any port. - isLoopbackBindHost: use net.ParseIP().IsLoopback() so the whole 127.0.0.0/8 range and ::1 are recognised, not just 127.0.0.1. - buildHTTPSecurityMiddleware: when the listener is bound to a loopback address and OAuth is off, relax the Origin default to "*" while keeping the Host check on the strict loopback-only path. This preserves the curl-from-localhost workflow without weakening the DNS-rebinding mitigation for non-loopback binds. - chart/values.yaml: drop the misleading "bind to loopback" option (the chart hard-codes 0.0.0.0) and document that allowedHosts entries may include a port. - tests: cover port-bearing and IPv6 bracketed allowlist entries, 127.0.0.0/8 loopback bind, the new loopback-Origin relaxation, and a strict-mode regression guard for non-loopback binds. - container_test: pass --allowed-host=localhost so the streamable-http smoke test satisfies the new fail-closed startup validator.
Summary
Test plan